package com.eeeffff.yapidoc.utils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.eeeffff.yapidoc.models.media.PrimitiveType;

public class ReflectionUtils {
	private static final Logger LOGGER = LoggerFactory.getLogger(ReflectionUtils.class);

	public static Type typeFromString(String type) {
		final PrimitiveType primitive = PrimitiveType.fromName(type);
		if (primitive != null) {
			return primitive.getKeyClass();
		}
		try {
			return loadClassByName(type);
		} catch (Exception e) {
			LOGGER.error(String.format("Failed to resolve '%s' into class", type), e);
		}
		return null;
	}

	/**
	 * Load Class by class name. If class not found in it's Class loader or one of
	 * the parent class loaders - delegate to the Thread's ContextClassLoader
	 *
	 * @param className Canonical class name
	 * @return Class definition of className
	 * @throws ClassNotFoundException
	 */
	public static Class<?> loadClassByName(String className) throws ClassNotFoundException {
		try {
			return Class.forName(className);
		} catch (ClassNotFoundException e) {
			return Thread.currentThread().getContextClassLoader().loadClass(className);
		}
	}

	/**
	 * Checks if the method methodToFind is the overridden method from the
	 * superclass or superinterface.
	 *
	 * @param methodToFind is method to check
	 * @param cls          is method class
	 * @return true if the method is overridden method
	 */
	public static boolean isOverriddenMethod(Method methodToFind, Class<?> cls) {
		Set<Class<?>> superClasses = new HashSet<>();
		for (Class c : cls.getInterfaces()) {
			superClasses.add(c);
		}

		if (cls.getSuperclass() != null) {
			superClasses.add(cls.getSuperclass());
		}

		for (Class<?> superClass : superClasses) {
			if (superClass != null && !(superClass.equals(Object.class))) {
				for (Method method : superClass.getMethods()) {
					if (method.getName().equals(methodToFind.getName())
							&& method.getReturnType().isAssignableFrom(methodToFind.getReturnType())
							&& Arrays.equals(method.getParameterTypes(), methodToFind.getParameterTypes())
							&& !Arrays.equals(method.getGenericParameterTypes(),
									methodToFind.getGenericParameterTypes())) {
						return true;
					}
				}
				if (isOverriddenMethod(methodToFind, superClass)) {
					return true;
				}
			}

		}
		return false;
	}

	/**
	 * Returns overridden method from superclass if it exists. If method was not
	 * found returns null.
	 *
	 * @param method is method to find
	 * @return overridden method from superclass
	 */
	public static Method getOverriddenMethod(Method method) {
		Class<?> declaringClass = method.getDeclaringClass();
		Class<?> superClass = declaringClass.getSuperclass();
		Method result = null;
		if (superClass != null && !(superClass.equals(Object.class))) {
			result = findMethod(method, superClass);
		}
		if (result == null) {
			for (Class<?> anInterface : declaringClass.getInterfaces()) {
				result = findMethod(method, anInterface);
				if (result != null) {
					return result;
				}
			}
		}
		return result;
	}

	/**
	 * Searches the method methodToFind in given class cls. If the method is found
	 * returns it, else return null.
	 *
	 * @param methodToFind is the method to search
	 * @param cls          is the class or interface where to search
	 * @return method if it is found
	 */
	public static Method findMethod(Method methodToFind, Class<?> cls) {
		if (cls == null) {
			return null;
		}
		String methodToSearch = methodToFind.getName();
		Class<?>[] soughtForParameterType = methodToFind.getParameterTypes();
		Type[] soughtForGenericParameterType = methodToFind.getGenericParameterTypes();
		for (Method method : cls.getMethods()) {
			if (method.getName().equals(methodToSearch)
					&& method.getReturnType().isAssignableFrom(methodToFind.getReturnType())) {
				Class<?>[] srcParameterTypes = method.getParameterTypes();
				Type[] srcGenericParameterTypes = method.getGenericParameterTypes();
				if (soughtForParameterType.length == srcParameterTypes.length
						&& soughtForGenericParameterType.length == srcGenericParameterTypes.length) {
					if (hasIdenticalParameters(srcParameterTypes, soughtForParameterType, srcGenericParameterTypes,
							soughtForGenericParameterType)) {
						return method;
					}
				}
			}
		}
		return null;
	}

	private static boolean hasIdenticalParameters(Class<?>[] srcParameterTypes, Class<?>[] soughtForParameterType,
			Type[] srcGenericParameterTypes, Type[] soughtForGenericParameterType) {
		for (int j = 0; j < soughtForParameterType.length; j++) {
			Class<?> parameterType = soughtForParameterType[j];
			if (!(srcParameterTypes[j].equals(parameterType)
					|| (!srcGenericParameterTypes[j].equals(soughtForGenericParameterType[j])
							&& srcParameterTypes[j].isAssignableFrom(parameterType)))) {
				return false;
			}
		}
		return true;
	}

	public static boolean isInject(List<Annotation> annotations) {
		for (Annotation annotation : annotations) {
			// use string name to avoid additional dependencies
			if ("javax.inject.Inject".equals(annotation.annotationType().getName())) {
				return true;
			}
		}
		return false;
	}

	public static boolean isConstructorCompatible(Constructor<?> constructor) {
		if (!Modifier.isPublic(constructor.getModifiers())) {
			final int access = Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE;
			return constructor.getParameterTypes().length == 0
					&& (constructor.getDeclaringClass().getModifiers() & access) == constructor.getModifiers();
		}
		return true;
	}

	/**
	 * Returns the list of declared fields from the class <code>cls</code> and its
	 * superclasses excluding <code>Object</code> class. If the field from child
	 * class hides the field from superclass, the field from superclass won't be
	 * added to the result list.
	 *
	 * @param cls is the processing class
	 * @return list of Fields
	 */
	public static List<Field> getDeclaredFields(Class<?> cls) {
		if (cls == null || Object.class.equals(cls)) {
			return Collections.emptyList();
		}
		final List<Field> fields = new ArrayList<Field>();
		final Set<String> fieldNames = new HashSet<String>();
		for (Field field : cls.getDeclaredFields()) {
			fields.add(field);
			fieldNames.add(field.getName());
		}
		for (Field field : getDeclaredFields(cls.getSuperclass())) {
			if (!fieldNames.contains(field.getName())) {
				fields.add(field);
			}
		}
		return fields;
	}

	/**
	 * Returns an annotation by type from a method.
	 *
	 * @param method          is the method to find
	 * @param annotationClass is the type of annotation
	 * @param <A>             is the type of annotation
	 * @return annotation if it is found
	 */
	public static <A extends Annotation> A getAnnotation(Method method, Class<A> annotationClass) {
		A annotation = method.getAnnotation(annotationClass);
		if (annotation == null) {
			for (Annotation metaAnnotation : method.getAnnotations()) {
				annotation = metaAnnotation.annotationType().getAnnotation(annotationClass);
				if (annotation != null) {
					return annotation;
				}
			}
			Method superclassMethod = getOverriddenMethod(method);
			if (superclassMethod != null) {
				annotation = getAnnotation(superclassMethod, annotationClass);
			}
		}
		return annotation;
	}

	public static <A extends Annotation> A getAnnotation(Class<?> cls, Class<A> annotationClass) {
		A annotation = cls.getAnnotation(annotationClass);
		if (annotation == null) {
			for (Annotation metaAnnotation : cls.getAnnotations()) {
				annotation = metaAnnotation.annotationType().getAnnotation(annotationClass);
				if (annotation != null) {
					return annotation;
				}
				;
			}
			Class<?> superClass = cls.getSuperclass();
			if (superClass != null && !(superClass.equals(Object.class))) {
				annotation = getAnnotation(superClass, annotationClass);
			}
		}
		if (annotation == null) {
			for (Class<?> anInterface : cls.getInterfaces()) {
				for (Annotation metaAnnotation : anInterface.getAnnotations()) {
					annotation = metaAnnotation.annotationType().getAnnotation(annotationClass);
					if (annotation != null) {
						return annotation;
					}
					;
				}
				annotation = getAnnotation(anInterface, annotationClass);
				if (annotation != null) {
					return annotation;
				}
			}
		}
		return annotation;
	}

	/**
	 * Returns a List of repeatable annotations by type from a method.
	 *
	 * @param method          is the method to find
	 * @param annotationClass is the type of annotation
	 * @param <A>             is the type of annotation
	 * @return List of repeatable annotations if it is found
	 */
	public static <A extends Annotation> List<A> getRepeatableAnnotations(Method method, Class<A> annotationClass) {
		A[] annotations = method.getAnnotationsByType(annotationClass);
		if (annotations == null || annotations.length == 0) {
			for (Annotation metaAnnotation : method.getAnnotations()) {
				annotations = metaAnnotation.annotationType().getAnnotationsByType(annotationClass);
				if (annotations != null && annotations.length > 0) {
					return Arrays.asList(annotations);
				}
			}
			Method superclassMethod = getOverriddenMethod(method);
			if (superclassMethod != null) {
				return getRepeatableAnnotations(superclassMethod, annotationClass);
			}
		}
		if (annotations == null) {
			return null;
		}
		return Arrays.asList(annotations);
	}

	public static <A extends Annotation> List<A> getRepeatableAnnotations(Class<?> cls, Class<A> annotationClass) {
		A[] annotations = getRepeatableAnnotationsArray(cls, annotationClass);
		if (annotations == null || annotations.length == 0) {
			return null;
		}
		return Arrays.asList(annotations);
	}

	public static <A extends Annotation> A[] getRepeatableAnnotationsArray(Class<?> cls, Class<A> annotationClass) {
		A[] annotations = cls.getAnnotationsByType(annotationClass);
		if (annotations == null || annotations.length == 0) {
			for (Annotation metaAnnotation : cls.getAnnotations()) {
				annotations = metaAnnotation.annotationType().getAnnotationsByType(annotationClass);
				if (annotations != null && annotations.length > 0) {
					return annotations;
				}
			}
			Class<?> superClass = cls.getSuperclass();
			if (superClass != null && !(superClass.equals(Object.class))) {
				annotations = getRepeatableAnnotationsArray(superClass, annotationClass);
			}
		}
		if (annotations == null || annotations.length == 0) {
			for (Class<?> anInterface : cls.getInterfaces()) {
				for (Annotation metaAnnotation : anInterface.getAnnotations()) {
					annotations = metaAnnotation.annotationType().getAnnotationsByType(annotationClass);
					if (annotations != null && annotations.length > 0) {
						return annotations;
					}
				}
				annotations = getRepeatableAnnotationsArray(anInterface, annotationClass);
				if (annotations != null) {
					return annotations;
				}
			}
		}
		return annotations;
	}

	public static Annotation[][] getParameterAnnotations(Method method) {
		Annotation[][] methodAnnotations = method.getParameterAnnotations();
		Method overriddenmethod = getOverriddenMethod(method);

		if (overriddenmethod != null) {
			Annotation[][] overriddenAnnotations = overriddenmethod.getParameterAnnotations();

			for (int i = 0; i < methodAnnotations.length; i++) {
				List<Type> types = new ArrayList<>();
				for (int j = 0; j < methodAnnotations[i].length; j++) {
					types.add(methodAnnotations[i][j].annotationType());
				}
				for (int j = 0; j < overriddenAnnotations[i].length; j++) {
					if (!types.contains(overriddenAnnotations[i][j].annotationType())) {
						methodAnnotations[i] = ArrayUtils.add(methodAnnotations[i], overriddenAnnotations[i][j]);
					}
				}

			}
		}
		return methodAnnotations;
	}

	/**
	 * Checks if the type is void.
	 *
	 * @param type is the type to check
	 * @return true if the type is void
	 */
	public static boolean isVoid(Type type) {
		final Class<?> cls = TypeFactory.defaultInstance().constructType(type).getRawClass();
		return Void.class.isAssignableFrom(cls) || Void.TYPE.isAssignableFrom(cls);
	}

	public static boolean isSystemType(JavaType type) {
		// used while resolving container types to skip resolving system types; possibly
		// extend by checking classloader
		// and/or other packages
		for (String systemPrefix : PrimitiveType.systemPrefixes()) {
			if (type.getRawClass().getName().startsWith(systemPrefix)) {
				if (!PrimitiveType.nonSystemTypes().contains(type.getRawClass().getName())
						&& !PrimitiveType.nonSystemTypePackages().contains(type.getRawClass().getPackage().getName())) {
					return true;
				}
			}
		}
		if (type.isArrayType()) {
			return true;
		}
		return false;
	}
}
